[wip] escape analysis 2#3213
Conversation
Move escapeAnalysis / scopeFreeOptimization out of the macro infer fixpoint and run them once after buildAccessFlags, where callee sideEffectFlags are final - groundwork for gating call-argument escape on the callee's real side effects instead of builtIn-only. Placed before lint/foldUnsafe so the re-infer of the inserted scope_free keeps the original ordering and does not re-trip already-folded unsafe checks. A single dirty re-type is the fixpoint (the inserted call is a generated terminal that creates no new candidate and changes no rws). Behavior-preserving: isEscapeNeutralCall still gates on builtIn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend escape analysis so a local `new`-pointer passed to a SCRIPT function can be freed at scope exit when the callee provably does not let that argument escape - previously only pure built-ins qualified. New Pass 0 (ParamEscapeAnalysis): an optimistic fixpoint over each analyzable function's by-value pointer-to-struct parameters. A parameter is escape-free unless its body leaks the pointer (return, store, capture into a closure, or pass to a non-neutral argument); the fixpoint lets the property transit call chains and converge on mutual recursion. Result lands on Variable::does_not_escape and is consumed by isArgEscapeNeutral at call sites (built-ins still judged by declared side effects). Also: comparison operands (==/!=) are escape-neutral, so a null-guard `p == null` no longer counts as a leaking use - this fixes both the new parameter pass and the existing local pass (a null-checked local is now freeable). Tests: pure/transitive script-call frees (heap stays flat) and soundness (store / transitive-store / closure-capture in a script callee must NOT free) under the validating collect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| if ( a->name==name && isParamEscapeCandidate(a) ) escaped.insert(a); | ||
| } | ||
| } | ||
| virtual void preVisit ( ExprField * expr ) override { |
| Visitor::preVisit(expr); | ||
| for ( auto & cap : expr->capture ) escapeByName(cap.name); | ||
| } | ||
| virtual void preVisit ( ExprMakeGenerator * expr ) override { |
There was a problem hiding this comment.
lambdas are also ExprMakeStruct via move semantics.
borisbat
left a comment
There was a problem hiding this comment.
needs more extensive testing for the cases specified. regular lambda. struct init by move. etc etc
There was a problem hiding this comment.
Pull request overview
Extends the compiler’s escape-free optimization to treat certain script-function calls as escape-neutral on a per-parameter basis (interprocedural fixpoint), adds regression tests for both “must not free” and “should free” scenarios, and adjusts when escape analysis/scope-free are run in the compilation pipeline.
Changes:
- Add interprocedural per-parameter escape analysis for script functions and use it to classify script call arguments as escape-neutral where proven safe.
- Move escape analysis + scope-free optimization out of the infer loop into
ast_parse.cppafterbuildAccessFlags, with a targeted dirty re-infer. - Add GC tests covering script-call escaping and script-call non-escaping behaviors.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/gc/test_gc_escape_free.das | Adds tests asserting locals passed to escaping script callees are not statically freed. |
| tests/gc/test_gc_escape_free_frees.das | Adds tests asserting locals passed to provably pure script callees are statically freed (heap stays flat). |
| src/ast/ast_parse.cpp | Moves escapeAnalysis/scopeFreeOptimization to the parse pipeline and performs a post-insertion dirty re-infer. |
| src/ast/ast_infer_type.cpp | Removes escapeAnalysis/scopeFreeOptimization invocation from the infer loop. |
| src/ast/ast_escape_analysis.cpp | Adds per-parameter fixpoint analysis for script functions and updates call-site neutrality checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // a SCRIPT callee that stores its argument into a global -> its parameter escapes, so a local | ||
| // passed only here must NOT be freed | ||
| def keep_node_script(var p : Node?) { g_kept = p } |
|
|
||
| // transitive: keep_node_script_t forwards p to keep_node_script, which stores it -> the escape | ||
| // transits the call chain, so the parameter still escapes | ||
| def keep_node_script_t(var p : Node?) { keep_node_script(p) } |
|
|
||
| // a script callee that captures its argument into a lambda stored in a global -> the parameter | ||
| // escapes through the capture frame | ||
| def keep_node_capture(var p : Node?) { g_lam <- @() : int { return p.x } } |
| program->escapeAnalysis(logs); | ||
| if ( program->scopeFreeOptimization(logs) ) { | ||
| inferTypesDirty(program.get(), logs, true); | ||
| if ( program->failed() ) { | ||
| program->error("internal compiler error: escape free optimization infer to fail", "", "", LineInfo(), CompilationError::internal_pod_analysis_infer); | ||
| } | ||
| } |
| // pass 0 first: interprocedural per-parameter escape, so pass 1 can free a local passed to a | ||
| // script function whose matching parameter provably does not escape | ||
| ParamEscapeAnalysis pe(logEscape ? &logs : nullptr); | ||
| pe.run(this); | ||
| EscapeAnalysisVisitor ev(logEscape ? &logs : nullptr); | ||
| visit(ev); | ||
| return ev.anyChanged; |
No description provided.